//---------------------------------------------------------------------
//Copyright (c) 2017-2021 Makito

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// URL:https://github.com/SumiMakito/Awesome-qr.js
//---------------------------------------------------------------------

/**
 * @description
 * 在 Awesome-qr.js 基础上主要改动：
 * 1. 将多个Canvas合并，并对其绘制过程进行重构
 * 2. 将导出图片的方法改为微信小程序导出图片的临时文件
 * 3. 修改loadImage方法，使其可以支持微信小程序
 * 4. 删除gif动画的支持
 */

import { getRoundNum, loadImage } from "./utils";
import { QRCodeModel, QRErrorCorrectLevel, QRUtil } from "./qrcode";

const defaultScale = 0.4;

export class AwesomeQR {
  canvas;
  canvasContext;
  qrCode;
  options;

  static CorrectLevel = QRErrorCorrectLevel;

  static defaultComponentOptions = {
    data: {
      scale: 1,
    },
    timing: {
      scale: 1,
      protectors: false,
    },
    alignment: {
      scale: 1,
      protectors: false,
    },
    cornerAlignment: {
      scale: 1,
      protectors: true,
    },
  };

  static defaultOptions = {
    text: "",
    size: 400,
    margin: 20,
    colorDark: "#000000",
    colorLight: "#ffffff",
    correctLevel: QRErrorCorrectLevel.M,
    backgroundImage: undefined,
    backgroundDimming: "rgba(0,0,0,0)",
    logoImage: undefined,
    logoScale: 0.2,
    logoMargin: 0,
    logoCornerRadius: 8,
    whiteMargin: false,
    components: AwesomeQR.defaultComponentOptions,
    autoColor: false,
  };

  constructor(options) {
    this.setOptions(options)
  }

  draw = () => {
    return new Promise((resolve) => this._draw().then(resolve));
  }
  setOptions = (options)=>{
    const _options = Object.assign({}, options);
    (Object.keys(AwesomeQR.defaultOptions)).forEach((k) => {
      if (!(k in _options)) {
        Object.defineProperty(_options, k, { value: AwesomeQR.defaultOptions[k], enumerable: true, writable: true });
      }
    });

    if (!_options.components) {
      _options.components = AwesomeQR.defaultComponentOptions;
    } else if (typeof _options.components === "object") {
      (Object.keys(AwesomeQR.defaultComponentOptions)).forEach((k) => {
        if (!(k in _options.components)) {
          Object.defineProperty(_options.components, k, {
            value: AwesomeQR.defaultComponentOptions[k],
            enumerable: true,
            writable: true,
          });
        } else {
          Object.defineProperty(_options.components, k, {
            value: { ...AwesomeQR.defaultComponentOptions[k], ..._options.components[k] },
            enumerable: true,
            writable: true,
          });
        }
      });
    }
    if (_options.dotScale !== null && _options.dotScale !== undefined) {
      if (_options.dotScale <= 0 || _options.dotScale > 1) {
        throw new Error("dotScale should be in range (0, 1].");
      }
      _options.components.data.scale = _options.dotScale;
      _options.components.timing.scale = _options.dotScale;
      _options.components.alignment.scale = _options.dotScale;
    }

    this.options = _options;

    this.canvas = options.canvasContainer.qrMainContainer
    this.canvasContext = this.canvas.getContext("2d");

    this.qrCode = new QRCodeModel(-1, this.options.correctLevel)
    if (Number.isInteger(this.options.maskPattern)) {
      this.qrCode.maskPattern = this.options.maskPattern;
    }
    if (Number.isInteger(this.options.version)) {
      this.qrCode.typeNumber = this.options.version;
    }
    this.qrCode.addData(this.options.text);
    this.qrCode.make();
    return _options;
  }
  _clear = () => {
    this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }

  static _prepareRoundedCornerClipReverse = (
    canvasContext,
    x,
    y,
    w,
    h,
    r,
    size
  ) => {
    canvasContext.beginPath();
    canvasContext.lineTo(0, 0);
    canvasContext.lineTo(0, size);
    canvasContext.lineTo(size, size);
    canvasContext.lineTo(size, 0);
    canvasContext.lineTo(0, 0);

    canvasContext.lineTo(x, y);
    canvasContext.arcTo(x + w, y, x + w, y + h, r);
    canvasContext.arcTo(x + w, y + h, x, y + h, r);
    canvasContext.arcTo(x, y + h, x, y, r);
    canvasContext.arcTo(x, y, x + w, y, r);
    canvasContext.closePath();
  }
  static _getAverageRGB = (image, options) => {
    const blockSize = 5;
    const defaultRGB = {
      r: 0,
      g: 0,
      b: 0,
    };
    let width, height;
    let i = -4;
    const rgb = {
      r: 0,
      g: 0,
      b: 0,
    };
    let count = 0;
    //  @ts-ignore
    height = image.naturalHeight || image.height;
    //  @ts-ignore
    width = image.naturalWidth || image.width;
    const canvas = options.canvasContainer.qrMainContainer
    const context = canvas.getContext("2d");

    if (!context) {
      return defaultRGB;
    }

    let data;
    try {
      data = context.getImageData(0, 0, width, height);
    } catch (e) {
      return defaultRGB;
    }

    while ((i += blockSize * 4) < data.data.length) {
      if (data.data[i] > 200 || data.data[i + 1] > 200 || data.data[i + 2] > 200) continue;
      ++count;
      rgb.r += data.data[i];
      rgb.g += data.data[i + 1];
      rgb.b += data.data[i + 2];
    }

    rgb.r = ~~(rgb.r / count);
    rgb.g = ~~(rgb.g / count);
    rgb.b = ~~(rgb.b / count);

    return rgb;
  }

  static _drawDot(
    canvasContext,
    centerX,
    centerY,
    nSize,
    xyOffset = 0,
    dotScale = 1
  ) {
    canvasContext.fillRect(
      (centerX + xyOffset) * nSize,
      (centerY + xyOffset) * nSize,
      dotScale * nSize,
      dotScale * nSize
    );
  }

  static _drawAlignProtector(
    canvasContext,
    centerX ,
    centerY ,
    nSize 
  ) {
    canvasContext.clearRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
    canvasContext.fillRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
  }

  static _drawAlign(
    canvasContext,
    centerX,
    centerY,
    nSize,
    xyOffset = 0,
    dotScale = 1,
    colorDark,
    hasProtector
  ) {
    const oldFillStyle = canvasContext.fillStyle;
    canvasContext.fillStyle = colorDark;
    new Array(4).fill(0).map((_, i) => {
      AwesomeQR._drawDot(canvasContext, centerX - 2 + i, centerY - 2, nSize, xyOffset, dotScale);
      AwesomeQR._drawDot(canvasContext, centerX + 2, centerY - 2 + i, nSize, xyOffset, dotScale);
      AwesomeQR._drawDot(canvasContext, centerX + 2 - i, centerY + 2, nSize, xyOffset, dotScale);
      AwesomeQR._drawDot(canvasContext, centerX - 2, centerY + 2 - i, nSize, xyOffset, dotScale);
    });
    AwesomeQR._drawDot(canvasContext, centerX, centerY, nSize, xyOffset, dotScale);
    if (!hasProtector) {
      canvasContext.fillStyle = "rgba(255, 255, 255, 0.6)";
      new Array(2).fill(0).map((_, i) => {
        AwesomeQR._drawDot(canvasContext, centerX - 1 + i, centerY - 1, nSize, xyOffset, dotScale);
        AwesomeQR._drawDot(canvasContext, centerX + 1, centerY - 1 + i, nSize, xyOffset, dotScale);
        AwesomeQR._drawDot(canvasContext, centerX + 1 - i, centerY + 1, nSize, xyOffset, dotScale);
        AwesomeQR._drawDot(canvasContext, centerX - 1, centerY + 1 - i, nSize, xyOffset, dotScale);
      });
    }
    canvasContext.fillStyle = oldFillStyle;
  }

   _draw = async () => {
    const nCount = this.qrCode?.moduleCount;
    
    // 传入宽高
    const rawSize = this.options.size;
    // 传入margin
    let rawMargin = this.options.margin;

    // 这种情况margin会置为0
    if (rawMargin < 0 || rawMargin * 2 >= rawSize) {
      rawMargin = 0;
    }

    // 取整margin
    const margin = Math.ceil(rawMargin);

    // 除去rawMargin的宽高
    const rawViewportSize = rawSize - 2 * rawMargin;


    // 二维码点的宽高
    const nSize = getRoundNum(rawViewportSize / nCount, 3)

    // =========================
    // 可视宽高
    const viewportSize = nSize * nCount;

    // 总宽高
    let size = getRoundNum(viewportSize + 2 * margin, 3);
    // =========================


    const mainCanvas = this.options.canvasContainer.qrMainContainer;
    const mainCanvasContext = mainCanvas.getContext("2d");

    this._clear();



    // Fill the margin
    if (this.options.whiteMargin) {
      mainCanvasContext.save(); // 保存一份默认的状态
      mainCanvasContext.fillStyle = "#FFFFFF";
      mainCanvasContext.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
      mainCanvasContext.restore(); // 恢复到上次默认的状态，接下来可以接着绘制，之前的不会被清空。但是如果重叠，会盖住之前的。

    }
    // Translate to make the top and left margins off the viewport
    mainCanvasContext.save();
    mainCanvasContext.translate(margin, margin);
    // mainCanvasContext.setTransform(1, 0, 0, 1, 0, 0);

    // const backgroundCanvas = new Canvas(size, size);
    const backgroundCanvas = this.options.canvasContainer.qrMainContainer;
    const backgroundCanvasContext = backgroundCanvas.getContext("2d");
    let backgroundImage;


    if (!!this.options.backgroundImage) {
      backgroundImage = await loadImage(backgroundCanvas, this.options.backgroundImage);
      backgroundCanvasContext.drawImage(
        // @ts-ignore
        backgroundImage,
        0,
        0,
        backgroundImage.width,
        backgroundImage.height,
        0,
        0,
        rawViewportSize,
        rawViewportSize
      );
      if (this.options.autoColor) {
        const avgRGB = AwesomeQR._getAverageRGB(backgroundImage, this.options);
        this.options.colorDark = `rgb(${avgRGB.r},${avgRGB.g},${avgRGB.b})`;
      }
      // 为什么有margin存在时，图片没有覆盖margin？
      backgroundCanvasContext.rect(-0, -0, size, size);
      backgroundCanvasContext.fillStyle = this.options.backgroundDimming;
      backgroundCanvasContext.fill();
    } else {
      backgroundCanvasContext.rect(-margin, -margin, size, size);
      backgroundCanvasContext.fillStyle = this.options.colorLight;
      backgroundCanvasContext.fill();
    }
    const alignmentPatternCenters = QRUtil.getPatternPosition(this.qrCode.typeNumber);
    const dataScale = this.options.components?.data?.scale || defaultScale;
    const dataXyOffset = (1 - dataScale) * 0.5;
    // 提前预备好logo margin的空
    if (!!this.options.logoImage && this.options.logoMargin) {
      let logoMargin = this.options.logoMargin;
      let logoScale = this.options.logoScale;
      let logoCornerRadius = this.options.logoCornerRadius;
      const logoSize = viewportSize * logoScale;
      const x = 0.5 * (size - logoSize);
      const y = x;
      AwesomeQR._prepareRoundedCornerClipReverse(
        mainCanvasContext,
        x - logoMargin - rawMargin,
        y - logoMargin - rawMargin,
        logoSize + 2 * logoMargin,
        logoSize + 2 * logoMargin,
        logoCornerRadius + logoMargin,
        size
      );
      mainCanvasContext.fill();
      mainCanvasContext.clip();
    }

    for (let row = 0; row < nCount; row++) {
      for (let col = 0; col < nCount; col++) {
        const bIsDark = this.qrCode.isDark(row, col);
        const isBlkPosCtr = (col < 8 && (row < 8 || row >= nCount - 8)) || (col >= nCount - 8 && row < 8);
        const isTiming = (row == 6 && col >= 8 && col <= nCount - 8) || (col == 6 && row >= 8 && row <= nCount - 8);
        let isProtected = isBlkPosCtr || isTiming;

        for (let i = 1; i < alignmentPatternCenters.length - 1; i++) {
          isProtected =
            isProtected ||
            (row >= alignmentPatternCenters[i] - 2 &&
              row <= alignmentPatternCenters[i] + 2 &&
              col >= alignmentPatternCenters[i] - 2 &&
              col <= alignmentPatternCenters[i] + 2);
        }

        const nLeft = col * nSize + (isProtected ? 0 : dataXyOffset * nSize);
        const nTop = row * nSize + (isProtected ? 0 : dataXyOffset * nSize);
        mainCanvasContext.strokeStyle = bIsDark ? this.options.colorDark : this.options.colorLight;
        mainCanvasContext.lineWidth = 0.5;
        mainCanvasContext.fillStyle = bIsDark ? this.options.colorDark : "rgba(255, 255, 255, 0.6)";
        if (alignmentPatternCenters.length === 0) {
          if (!isProtected) {
            mainCanvasContext.fillRect(
              nLeft,
              nTop,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize
            );
          }
        } else {
          const inAgnRange = col < nCount - 4 && col >= nCount - 4 - 5 && row < nCount - 4 && row >= nCount - 4 - 5;
          if (!isProtected && !inAgnRange) {
            // if align pattern list is empty, then it means that we don't need to leave room for the align patterns
            mainCanvasContext.fillRect(
              nLeft,
              nTop,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize,
              (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize
            );
          }
        }
      }
    }
    const cornerAlignmentCenter = alignmentPatternCenters[alignmentPatternCenters.length - 1];

    // - PROTECTORS
    const protectorStyle = "rgba(255, 255, 255, 0.6)";

    // - FINDER PROTECTORS
    mainCanvasContext.fillStyle = protectorStyle;
    mainCanvasContext.fillRect(0, 0, 8 * nSize, 8 * nSize);
    mainCanvasContext.fillRect(0, (nCount - 8) * nSize, 8 * nSize, 8 * nSize);
    mainCanvasContext.fillRect((nCount - 8) * nSize, 0, 8 * nSize, 8 * nSize);
    // - TIMING PROTECTORS
    if (this.options.components?.timing?.protectors) {
      mainCanvasContext.fillRect(8 * nSize, 6 * nSize, (nCount - 8 - 8) * nSize, nSize);
      mainCanvasContext.fillRect(6 * nSize, 8 * nSize, nSize, (nCount - 8 - 8) * nSize);
    }

    // - CORNER ALIGNMENT PROTECTORS
    if (this.options.components?.cornerAlignment?.protectors) {
      AwesomeQR._drawAlignProtector(mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize);
    }

    // - ALIGNMENT PROTECTORS
    if (this.options.components?.alignment?.protectors) {
      for (let i = 0; i < alignmentPatternCenters.length; i++) {
        for (let j = 0; j < alignmentPatternCenters.length; j++) {
          const agnX = alignmentPatternCenters[j];
          const agnY = alignmentPatternCenters[i];
          if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
            continue;
          } else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
            continue;
          } else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
            continue;
          } else {
            AwesomeQR._drawAlignProtector(mainCanvasContext, agnX, agnY, nSize);
          }
        }
      }
    }

    // - FINDER
    mainCanvasContext.fillStyle = this.options.colorDark;
    mainCanvasContext.fillRect(0, 0, 7 * nSize, nSize);
    mainCanvasContext.fillRect((nCount - 7) * nSize, 0, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, 6 * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect((nCount - 7) * nSize, 6 * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, (nCount - 7) * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, (nCount - 7 + 6) * nSize, 7 * nSize, nSize);
    mainCanvasContext.fillRect(0, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect(6 * nSize, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect((nCount - 7) * nSize, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect((nCount - 7 + 6) * nSize, 0, nSize, 7 * nSize);
    mainCanvasContext.fillRect(0, (nCount - 7) * nSize, nSize, 7 * nSize);
    mainCanvasContext.fillRect(6 * nSize, (nCount - 7) * nSize, nSize, 7 * nSize);

    mainCanvasContext.fillRect(2 * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
    mainCanvasContext.fillRect((nCount - 7 + 2) * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
    mainCanvasContext.fillRect(2 * nSize, (nCount - 7 + 2) * nSize, 3 * nSize, 3 * nSize);

    // - TIMING
    const timingScale = this.options.components?.timing?.scale || defaultScale;
    const timingXyOffset = (1 - timingScale) * 0.5;
    for (let i = 0; i < nCount - 8; i += 2) {
      AwesomeQR._drawDot(mainCanvasContext, 8 + i, 6, nSize, timingXyOffset, timingScale);
      AwesomeQR._drawDot(mainCanvasContext, 6, 8 + i, nSize, timingXyOffset, timingScale);
    }
    // - CORNER ALIGNMENT PROTECTORS
    const cornerAlignmentScale = this.options.components?.cornerAlignment?.scale || defaultScale;
    const cornerAlignmentXyOffset = (1 - cornerAlignmentScale) * 0.5;
    AwesomeQR._drawAlign(
      mainCanvasContext,
      cornerAlignmentCenter,
      cornerAlignmentCenter,
      nSize,
      cornerAlignmentXyOffset,
      cornerAlignmentScale,
      this.options.colorDark,
      this.options.components?.cornerAlignment?.protectors || false
    );

    // - ALIGNEMNT
    const alignmentScale = this.options.components?.alignment?.scale || defaultScale;
    const alignmentXyOffset = (1 - alignmentScale) * 0.5;
    for (let i = 0; i < alignmentPatternCenters.length; i++) {
      for (let j = 0; j < alignmentPatternCenters.length; j++) {
        const agnX = alignmentPatternCenters[j];
        const agnY = alignmentPatternCenters[i];
        if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
          continue;
        } else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
          continue;
        } else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
          continue;
        } else {
          AwesomeQR._drawAlign(
            mainCanvasContext,
            agnX,
            agnY,
            nSize,
            alignmentXyOffset,
            alignmentScale,
            this.options.colorDark,
            this.options.components?.alignment?.protectors || false
          );
        }
      }
    }

    if (!!this.options.logoImage) {
      const logoImage = await loadImage(mainCanvas, this.options.logoImage);

      let logoScale = this.options.logoScale;
      let logoMargin = this.options.logoMargin;
      let logoCornerRadius = this.options.logoCornerRadius;
      if (logoScale <= 0 || logoScale >= 1.0) {
        logoScale = 0.2;
      }
      if (logoMargin < 0) {
        logoMargin = 0;
      }
      if (logoCornerRadius < 0) {
        logoCornerRadius = 0;
      }

      const logoSize = viewportSize * logoScale;
      const x = 0.5 * (size - logoSize);
      const y = x;


      mainCanvasContext.restore();
      // @ts-ignore
      mainCanvasContext.drawImage(logoImage, x, y, logoSize, logoSize);

    }
    this.qrCode = undefined;
    this.canvas = mainCanvas;
    return new Promise((reslove, reject) => {
      wx.canvasToTempFilePath({
        canvas: this.canvas,
        quality: 1,
        destWidth: this.canvas.width,
        destHeight: this.canvas.height
      }).then(rsp => {
        reslove(rsp.tempFilePath)
      }).catch(err => {
        console.error(COMPONENT_NAME,'canvasToTempFilePath 失败', err);
        reject(err)
      })
    })

  }
  getDataUrl(type = 'png', encoderOptions = 1) {
    return this.canvas.toDataURL(type, encoderOptions)
  }
}
